上一篇我們使用ReactiveCrudRepository來對資料庫存取,對於一些不太複雜的SQL指令來說,使用CrudRepository方便又省事,讓我們來看看在Reactive的世界中,是否仍然始終如一呢?
Spring提供另外一個R2dbcRepository,ReactiveSortingRepository繼承ReactiveCrudRepository,所以整體來說R2dbcRepository的功能會更加的全面。
public interface R2dbcRepository<T, ID> extends ReactiveSortingRepository<T, ID>, ReactiveQueryByExampleExecutor<T> {}
從上一篇的範例稍微調整一下(先替換為R2dbcRepository),與Spring Data最明顯的差異可想而知就是回傳的型別,
Optional<>改為Mono<>,多筆則換成了Flux<>。Publisher的方法,可以用stream的方式一個一個傳入message來查詢。page),雖然有提供Pageable 可以達到分頁的效果,但回傳的類別仍然是Flux<>而不是原本的Page,當然是可以額外自己count,推測Spring Data R2DBC沒有特別處理這個的原因或許是分頁某種程度上就是避免大量資料造成效能相關問題,但Reactive本身就有一些方式能解決這樣的問題。public interface GreetingRepository extends R2dbcRepository<Greeting, Long> {
  Mono<Greeting> findById(Long id);
  Flux<Greeting> findAll();
  Flux<Greeting> findByMessage(String message, Pageable pageable);
  Flux<Greeting> findByMessage((Publisher<String> message);
  Mono<Void> save(Mono<Greeting> greeting);
}
詳細可參考doc
針對修改類型的有三種回傳的型態
@Modifying的annotation,原本的Spring data一樣也需要。Mono<Integer> deleteByMessage(String message);
//Mono<Void> deleteByMessage(String message);
//Mono<Boolean> deleteByMessage(String message);
@Modifying
@Query("UPDATE message SET message = :message where id = :id")
Mono<Integer> updateMessage(String message, Long id);
仍有支援ID Generation,上一個範例就是透過mySql 自動產生流水號ID。判斷entity是否已存在DB的方式與Spring Data相同,常見的方式如下:
@ID:被掛上@ID的欄位是否為空,若為null則該entity會被視為一個新的entity,就會使用insert的語法反之則會用update。Version:被掛上@Version如果是0或是null,則被視為null。Persistable介面:自行實作isNew(),Spring data會透過isNew()來判斷。最後根據issue目前還不支援組合鍵(compositeId)。
或多或少都會有需求要客製物件,現在假設原本的Greeting需要多兩個欄位,一個單純的Y、N,另一個則是在DB裡面希望顯示數字,而程式則需顯示英文,先不討論需求合理性與有沒有別的作法,單純就是DEMO一次客製轉換的方式。
首先新增兩個Enum,在這邊預設甚麼都沒加,enum會轉換成string,這在以前記得Enum預設是轉換為enum的數字(順序)而不是文字。另一個則是希望DB儲存是自訂欄位而不是文字,所以增加了一個typeCode的屬性。
public enum YesNo { 
  Y, 
  N 
  ; 
}
@Getter 
public enum MessageType { 
  VOICE("1"), 
  MP3("2"), 
  WORD("3") 
  ; 
  private String typeCode; 
  MessageType(String typeCode) { 
    this.typeCode = typeCode; 
  } 
  private static final Map<String, MessageType> BY_CODE = new HashMap<>(MessageType.values().length); 
  static { 
    for (MessageType e : MessageType.values()) { 
      BY_CODE.put(e.getTypeCode(), e); 
    } 
  } 
  public static MessageType getTypeFromCode(String typeCode) { 
    return BY_CODE.getOrDefault(typeCode, WORD); 
  } 
}
public class Greeting {
  @Id
  private Long id;
  private String message;
  private YesNo isSend;
  private MessageType messageType;
  
  public Greeting(String message) {
    this.message = message;
  }
  public Greeting(Long id, String message) {
    this.id = id;
    this.message = message;
  }
  public Greeting(String message, YesNo isSend,
      MessageType messageType) {
    this.message = message;
    this.isSend = isSend;
    this.messageType = messageType;
  }
}
現在就需要特別設定自定義的Converter以及註冊到R2dbcConfiguration中,分別有
AppConfig:註冊。MessageTypeReadConverter:從db轉回物件,要注意converter是springframework.core裡面的。MessageTypeWriteConverter:從物件寫入db。@ReadingConverter
public class MessageTypeReadConverter
    implements org.springframework.core.convert.converter.Converter<String, MessageType> {
  @Override
  public MessageType convert(String source) {
    return MessageType.getTypeFromCode(source);
  }
}
@WritingConverter
public class MessageTypeWriteConverter implements Converter<MessageType, String> {
  @Override
  public String convert(MessageType source) {
    return source.getTypeCode();
  }
}
@Configuration
public class AppConfig extends AbstractR2dbcConfiguration {
  @Override
  public ConnectionFactory connectionFactory() {
    return ConnectionFactories.get("r2dbc:mysql://localhost:3306/test");
  }
  @Override
  protected List<Object> getCustomConverters() {
    return List.of(
        new MessageTypeReadConverter(),
        new MessageTypeWriteConverter()
    );
  }
}
測試後成功將資料寫入DB時會進行轉換,附上實際DB資料。
這次的實作感想是覺得Spring對於RDB的支援度還不夠全面,對於NoSql比較友善,像是這次使用mySql的R2DBC Driver  0.8.2,R2DBC官方DOC,是用OutboundRow來做轉換,但mySql R2DBC Driver的codec並不認識OutboundRow還需要自己再轉換一次,所以上面的範例索性就直接用String而不是OutboundRow,但這樣是如果情境更複雜需要多欄位組合就沒有辦法。
@WritingConverter 
public class PersonWriteConverter implements Converter<Person, OutboundRow> { 
  public OutboundRow convert(Person source) { 
    OutboundRow row = new OutboundRow(); 
    row.put("id", SettableValue.from(source.getId())); 
    row.put("name", SettableValue.from(source.getFirstName())); 
    row.put("age", SettableValue.from(source.getAge())); 
    return row; 
  } 
}